Skip to content

feat: Part C - Server auth enforcement (access_level, cookie auth, reCAPTCHA, JWT claims)#991

Merged
pyramation merged 4 commits intomainfrom
devin/1776459081-part-c-server-auth-enforcement
Apr 17, 2026
Merged

feat: Part C - Server auth enforcement (access_level, cookie auth, reCAPTCHA, JWT claims)#991
pyramation merged 4 commits intomainfrom
devin/1776459081-part-c-server-auth-enforcement

Conversation

@pyramation
Copy link
Copy Markdown
Contributor

@pyramation pyramation commented Apr 17, 2026

Summary

Server-side companion to the DB-layer auth changes in constructive-db PR #832. Adds four capabilities to the GraphQL server middleware:

C1 – access_level enforcement: When authenticate() returns access_level = 'read_only' (e.g. read-only API keys), the graphile middleware now sets default_transaction_read_only = 'on' in pgSettings, making Postgres reject writes at the transaction level.

C2 – Cookie-based session auth: The auth middleware now falls back to parsing a constructive_session cookie from the raw Cookie header when no Authorization: Bearer header is present. This avoids adding cookie-parser as a dependency.

C3 – reCAPTCHA middleware: New captcha.ts middleware gates protected mutations (signUp, resetPassword, etc.) behind Google reCAPTCHA verification when enable_captcha is true in auth settings. The secret key is read from RECAPTCHA_SECRET_KEY env var; the public site key lives in app_auth_settings for the frontend. Fully optional — no-op when either the setting or env var is absent.

C4 – kind + access_level as JWT claims: The graphile middleware now propagates jwt.claims.access_level and jwt.claims.kind into pgSettings, making them available to all PG functions via current_setting('jwt.claims.access_level') and current_setting('jwt.claims.kind'). This lets the DB layer make decisions based on credential type (api_key vs session) and access level (read_write vs read_only) without additional lookups.

Auth settings discovery: Settings are loaded dynamically from the tenant DB by joining metaschema_modules_public.sessions_module with metaschema_public.schema (both public schemas) to resolve the auth settings table's schema and name. No table names are hardcoded and no private schemas are accessed. Fails gracefully if modules or table don't exist yet (pre-migration). Discovery + settings fetch is 2 queries, cached in svcCache after first resolution.

Note: CORS migration (moving from api_modules to app_auth_settings.allowed_origins) has been deferred to a separate PR — CORS is more of a server/transport concern than an auth concern.

Review & Testing Checklist for Human

  • SQL identifier interpolation in AUTH_SETTINGS_SQL: schemaName and tableName are interpolated directly into the query string (line ~105 of api.ts). They come from metaschema_modules_public.sessions_module and metaschema_public.schema (trusted metaschema data), but verify this assumption holds. Consider whether pg identifier quoting (e.g. pg-format) would be prudent.
  • Cookie parsing robustness: parseCookieToken calls decodeURIComponent which can throw on malformed %-sequences. There is no try/catch around this call. Confirm this won't crash the auth middleware for garbage cookie values.
  • JWT claims side-effects: jwt.claims.access_level and jwt.claims.kind are now set for all authenticated requests. Verify that no existing PG functions call current_setting('jwt.claims.access_level', ...) or current_setting('jwt.claims.kind', ...) and make assumptions about these being absent. The values are only set when the token has them (guarded by if), but once set they affect the entire PG session.
  • CAPTCHA body parsing timing: The captcha middleware reads req.body.operationName, but it runs before graphile. Verify that express.json() or another body parser runs upstream so req.body is populated — otherwise the CAPTCHA gate may silently pass all requests through.
  • Deployment ordering: access_level and kind on the token depend on the updated authenticate() function from constructive-db PR fix(codegen): handle array types, computed fields, and orderBy enums in input-types-generator #832. Confirm that DB migration is deployed before/alongside this server change, or that the optional field handling is sufficient.

Recommended test plan

  1. Deploy with a tenant that has sessions_module + app_settings_auth provisioned → verify auth settings are loaded via the 2-step public-schema discovery
  2. Create a read-only API key → verify mutations are rejected by Postgres (cannot execute ... in a read-only transaction)
  3. Set a constructive_session cookie → verify auth succeeds without a Bearer header
  4. Enable enable_captcha + set RECAPTCHA_SECRET_KEY → verify sign-up is gated, sign-in is not
  5. Test against a tenant DB that has not been migrated → verify graceful fallback (no errors, anonymous auth proceeds)
  6. Authenticate with an API key → run SELECT current_setting('jwt.claims.kind') in a PG function → verify it returns 'api_key'

Notes

  • No unit tests are included; these are middleware integration changes that would benefit from e2e testing against a running server.
  • The captcha middleware returns HTTP 200 with GraphQL-shaped { errors: [...] } responses, consistent with how graphile reports errors.
  • CORS migration was intentionally deferred to a follow-up PR to keep scope focused on auth enforcement and reCAPTCHA.
  • Companion DB PR: constructive-db#832 (Part B – PL/pgSQL authenticate with last_used_at, access_level, credential_kind).

Link to Devin session: https://app.devin.ai/sessions/12acfda2a5434d2686c63515cfeb2610
Requested by: @pyramation

…S migration, reCAPTCHA

C1: access_level enforcement via SET TRANSACTION READ ONLY in graphile middleware
C2: Cookie-based session auth as fallback when no Bearer token present
C3: CORS migration from api_modules to app_auth_settings.allowed_origins with backward compat
C4: reCAPTCHA verification middleware for protected mutations (sign-up, password reset)
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

…tion

- Replace hardcoded app_settings_auth table name with dynamic discovery
  via metaschema_modules_public.sessions_module -> metaschema.schema_and_table()
- Remove allowed_origins from AuthSettings (CORS migration deferred)
- Revert cors.ts to legacy api_modules-only approach
@devin-ai-integration devin-ai-integration Bot changed the title feat: Part C - Server auth enforcement (access_level, cookie auth, CORS, reCAPTCHA) feat: Part C - Server auth enforcement (access_level, cookie auth, reCAPTCHA) Apr 17, 2026
Replace metaschema.schema_and_table() (private schema) with a JOIN
through metaschema_modules_public.sessions_module and metaschema_public.schema
(both public). Also reduces from 3 queries to 2 by combining the
module lookup and schema resolution into a single query.
Adds jwt.claims.access_level and jwt.claims.kind to PostgreSQL session
settings so PG functions can read them via current_setting(). This lets
the DB layer make decisions based on credential type (api_key vs session)
and access level (read_write vs read_only) without additional lookups.
@devin-ai-integration devin-ai-integration Bot changed the title feat: Part C - Server auth enforcement (access_level, cookie auth, reCAPTCHA) feat: Part C - Server auth enforcement (access_level, cookie auth, reCAPTCHA, JWT claims) Apr 17, 2026
@pyramation pyramation merged commit 647b999 into main Apr 17, 2026
51 checks passed
@pyramation pyramation deleted the devin/1776459081-part-c-server-auth-enforcement branch April 17, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant